﻿// --------------------------------
// <copyright file="SourceCodeGenerator.cs" company="TRONSoft">
//     Copyright (c) TRONSoft.
// </copyright>
// <license>
//   This source code is subject to terms and conditions of the Microsoft
//   Public License. A copy of the license can be found in the License.html
//   file at the root of this distribution. If you cannot locate the
//   Microsoft Public License, please send an email to dlr@microsoft.com.
//   By using this source code in any fashion, you are agreeing to be bound 
//   by the terms of the Microsoft Public License. You must not remove this
//   notice, or any other, from this software.
// </license>
// <author>Tron</author>
// ---------------------------------

using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using Refly.CodeDom;
using Refly.CodeDom.Expressions;
using Refly.CodeDom.Statements;
using Tronsoft.Actucal.CalculationEngineCreator.ScriptConfiguration;
using Tronsoft.Actucal.Tables;

//using System.CodeDom;
//using System.CodeDom.Compiler;

namespace Tronsoft.Actucal.CalculationEngineCreator
{
    public sealed class SourceCodeGenerator
    {
        private const string FIELD_PARAMETER_NAME = "parameters";
        private const string PROPERTY_PARAMETER_NAME = "Parameters";
        private const string ACTUCAL_CALCULATION_NAMESPACE = "Actucal.Calculation";
        private const string INTRINSIC_OBJECTS_ASSEMBLIES = "intrinsicObjectAssemblies";
        private const string FIELD_CACHED_RESULTS = "cachedResults";

        private readonly CalculationModel model;
        private readonly ActucalSection section;
        private FieldDeclaration cachedResults;

        public SourceCodeGenerator(CalculationModel model)
        {
            if (model == null) throw new ArgumentNullException("model");

            this.model = model;

            // Read the configuration file 
            try
            {
                Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                section = config.GetSection(ConfigurationSectionName.ACTUCAL_SERVICE) as ActucalSection;
            }
            catch (ConfigurationException ex)
            {
                Trace.WriteLine("Unable to load ActucalSection.");
                Trace.WriteLine(ex.ToString());
            }
        }

        public string GetSourceCode()
        {
            // creating the Refly namespace
            NamespaceDeclaration namespaceDeclaration = new NamespaceDeclaration(ACTUCAL_CALCULATION_NAMESPACE);

            // create the user class
            IClassDeclaration classDeclaration = namespaceDeclaration.AddClass(model.CALCULATION_ENGINE_CLASSNAME);

            // Add includes 
            AddIncludes(namespaceDeclaration);

            // Add constants to the source
            AddConstants(classDeclaration);

            // Add List of assemblies
            FieldDeclaration intrinsicObjectAssemblies =
                classDeclaration.AddField(typeof(Dictionary<string, Assembly>), INTRINSIC_OBJECTS_ASSEMBLIES);
            intrinsicObjectAssemblies.InitExpression = Expr.New(typeof (Dictionary<string, Assembly>));

            // Add dictionary with the cached results of a calculation
            cachedResults = classDeclaration.AddField(typeof(Dictionary<string, double>), FIELD_CACHED_RESULTS);
            this.cachedResults.InitExpression = Expr.New(typeof(Dictionary<string, double>));
            
            // add name field
            FieldDeclaration parameters =
                classDeclaration.AddField(typeof (CalculationParameterCollection), FIELD_PARAMETER_NAME);

            // add property
            PropertyDeclaration property = classDeclaration.AddProperty(parameters, PROPERTY_PARAMETER_NAME, true, false, false);
            property.Attributes = MemberAttributes.Public | MemberAttributes.Final;

            // Add the constructor
            AddConstructor(classDeclaration, parameters);

            // Add identifiers
            AddCalculationIdentifiers(classDeclaration);

            // Add methods
            AddMethods(classDeclaration);

            // Add cache reset method
            AddCacheResetMethod(classDeclaration);

            // Add tables
            AddTables(classDeclaration);

            // Add intrinsic objects
            if (section != null) 
            {
                foreach (IntrinsicElement intrinsicObjectElement in section.IntrinsicObjects)
                {
                    //FieldDeclaration intrinsicObjectField = classDeclaration.AddField(Type.GetType(intrinsicObjectElement.Type), intrinsicObjectElement.Name + "Field");
                    PropertyDeclaration intrinsicObjectProperty =
                        classDeclaration.AddProperty(intrinsicObjectElement.Class, intrinsicObjectElement.Name);
                    intrinsicObjectProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                    intrinsicObjectProperty.Set.Clear();
                    intrinsicObjectProperty.Get.Add(Expr.Snippet(@"Dim asm As [Assembly]"));
                    intrinsicObjectProperty.Get.Add(
                        Expr.Snippet(string.Format(@"If Not {0}.ContainsKey(""{1}"") Then",
                                                   intrinsicObjectAssemblies.Name, intrinsicObjectElement.Name)));
                    intrinsicObjectProperty.Get.Add(
                        Expr.Snippet(string.Format(@"   asm = Assembly.LoadFile(""{0}"")",
                                                   intrinsicObjectElement.Assembly)));
                    intrinsicObjectProperty.Get.Add(
                        Expr.Snippet(string.Format(@"   {0}.Add(""{1}"", asm)", intrinsicObjectAssemblies.Name,
                                                   intrinsicObjectElement.Name)));
                    intrinsicObjectProperty.Get.Add(Expr.Snippet(@"Else"));
                    intrinsicObjectProperty.Get.Add(
                        Expr.Snippet(string.Format(@" asm = {0}(""{1}"")", intrinsicObjectAssemblies.Name,
                                                   intrinsicObjectElement.Name)));
                    intrinsicObjectProperty.Get.Add(Expr.Snippet(@"End If"));
                    intrinsicObjectProperty.Get.Return(
                        Expr.Snippet(string.Format(@"TryCast(asm.CreateInstance(""{0}""), {0})",
                                                   intrinsicObjectElement.Class)));

                    //VariableDeclarationStatement assemblyVar = Stm.Var(typeof (Assembly), "asm");
                    //intrinsicObjectProperty.Get.Add(assemblyVar);
                    //intrinsicObjectProperty.Get.Add(
                    //    Stm.If(
                    //        Expr.False.Field(intrinsicObjectAssemblies).Method("ContainsKey").Invoke(
                    //            Expr.Prim(intrinsicObjectElement.Name)), Stm.Assign(Expr.Var(assemblyVar), Expr.Type(typeof(Assembly)).Method("LoadFile").Invoke(Expr.Prim(intrinsicObjectElement.Assembly))), 
                    //            Stm.));
                }
            }

            // Go and generate the code
            CodeGenerator gen = new CodeGenerator();
            gen.Provider = CodeGenerator.VbProvider;

            return gen.GenerateCode(namespaceDeclaration);
        }

        private void AddCacheResetMethod(IClassDeclaration classDeclaration)
        {
            MethodDeclaration method = classDeclaration.AddMethod("ResetCache");
            method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            method.Body.Add(Expr.Snippet(string.Format("{0}.Clear()", cachedResults.Name)));
        }

        private void AddIncludes(NamespaceDeclaration namespaceDeclaration)
        {
            if (namespaceDeclaration == null) throw new ArgumentNullException("namespaceDeclaration");

            // Add the includes
            foreach (string include in model.Includes)
            {
                namespaceDeclaration.Imports.Add(include);
            }
        }

        private void AddConstructor(IClassDeclaration classDeclaration, FieldDeclaration parameters)
        {
            if (classDeclaration == null) throw new ArgumentNullException("classDeclaration");
            if (parameters == null) throw new ArgumentNullException("parameters");

            // add constructor
            IConstructorDeclaration constructorDeclaration = classDeclaration.AddConstructor();
            // add name parameter
            ParameterDeclaration pname =
                constructorDeclaration.Signature.Parameters.Add(typeof (CalculationParameterCollection), FIELD_PARAMETER_NAME,
                                                                true);

            constructorDeclaration.Body.Add(Stm.ThrowIfNull(pname));

            constructorDeclaration.Body.AddAssign(
                Expr.This.Field(parameters),
                Expr.Arg(pname)
                );

            // Add parameter properties
            AddParameters(classDeclaration, constructorDeclaration);
            
            // A comment
            constructorDeclaration.Body.Add(Stm.Comment("Add the table data to the tablemanager."));
            // Get the resource stream 
            Expression resourceStream = Expr.This.Method("GetType").Prop("Assembly").Method("GetManifestResourceStream").Invoke(Expr.Prim(Resources.TABLES_DATA));
            VariableDeclarationStatement resourceStreamVar = Stm.Var(typeof(Stream), "resStream", resourceStream);
            
            // Add the streamreader var
            VariableDeclarationStatement streamReaderVar = Stm.Var(typeof(StreamReader), "sr", Expr.New(typeof(StreamReader), Expr.Var(resourceStreamVar)));
            VariableReferenceExpression streamReaderExpr = Expr.Var(streamReaderVar);

            // Feed the stream to the table manager            
            Expression tableMgrInstanceReadXml = Expr.Type(typeof(TableManager)).Method("Instance").Method("ReadXml").Invoke(streamReaderExpr);

            // Add it the the constructor body
            constructorDeclaration.Body.Add(resourceStreamVar);
            constructorDeclaration.Body.Add(Stm.IfNotNull(Expr.Var(resourceStreamVar), streamReaderVar, Stm.ToStm(tableMgrInstanceReadXml)));
        }

        private void AddParameters(IClassDeclaration classDeclaration, IConstructorDeclaration constructorDeclaration)
        {
            if (classDeclaration == null) throw new ArgumentNullException("classDeclaration");
            if (constructorDeclaration == null) throw new ArgumentNullException("constructorDeclaration");

            foreach (ICalculationParameter parameter in model.CalculationParameters)
            {
                if (null == parameter)
                    continue;

                // add property for parameter
                PropertyDeclaration property = classDeclaration.AddProperty(parameter.Type, parameter.Name);
                property.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                property.Get.Add(Stm.Return(Expr.This.Prop(PROPERTY_PARAMETER_NAME).Item(Expr.Prim(parameter.Name)).Prop("Value").Cast(parameter.Type)));

                if (IsPrimitiveDatatype(parameter.Type))
                {
                    property.Set.Add(Stm.Assign(Expr.This.Prop(PROPERTY_PARAMETER_NAME).Item(Expr.Prim(parameter.Name)).Prop("Value"), Expr.Snippet("Value")));
                }
            }          
        }
        
        private void AddConstants(IClassDeclaration classDeclaration)
        {
            if (classDeclaration == null) throw new ArgumentNullException("classDeclaration");

            foreach (INameValueCombination aConstant in model.Constants)
            {
                if (null == aConstant)
                    continue;

                Type constantValueType = aConstant.Value.GetType();
                if ((null == aConstant.Value) || IsPrimitiveDatatype(constantValueType))
                {
                    classDeclaration.AddConstant(constantValueType, aConstant.Name,
                                                 new PrimitiveExpression(aConstant.Value), false);
                }
                else if (typeof (DateTime) == constantValueType)
                {
                    classDeclaration.AddConstant(constantValueType, aConstant.Name,
                                                 new SnippetExpression(
                                                     string.Format("#{0}#",
                                                                   ((DateTime) aConstant.Value).ToString(
                                                                       CultureInfo.InvariantCulture))), false);
                }
            }
        }

        private void AddCalculationIdentifiers(IClassDeclaration classDeclaration)
        {
            if (classDeclaration == null) throw new ArgumentNullException("classDeclaration");

            foreach (Calculation calculation in model.Calculations)
            {
                PropertyDeclaration property = classDeclaration.AddProperty(typeof (double), calculation.Name);
                property.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                StringReader sr = new StringReader(calculation.Formula.Value/*.Replace("${", "Me._parameters(\"").Replace("}", "\").Value")*/);
                string line = String.Empty;

                TryCatchFinallyStatement exceptionStmt = Stm.Try();
                CatchClause catchException = Stm.Catch(typeof(Exception), "ex");
                catchException.Statements.Add(Expr.Type(typeof(Trace)).Method("WriteLine").Invoke(catchException.LocalParam));
                catchException.Statements.Add(
                    Stm.Throw(typeof(CalculationExecutionException), 
                    Expr.Snippet(String.Format("\"{0}\"", property.Name)), 
                    Expr.Snippet(catchException.LocalParam.Name)
                    ));
                exceptionStmt.CatchClauses.Add(catchException);
                property.Get.Add(exceptionStmt);

                // Add cache code
                // If 
                exceptionStmt.Try.Add(
                    Expr.Snippet(string.Format("If {0}.ContainsKey(\"{1}\") Then", cachedResults.Name, property.Name)));
                // Code in If
                exceptionStmt.Try.Add(Expr.Snippet(string.Format("\tReturn {0}(\"{1}\")", cachedResults.Name, property.Name)));     
                // Else
                exceptionStmt.Try.Add(Expr.Snippet(string.Format("Else")));
                // Code in else                
                while (null != (line = sr.ReadLine()))
                {
                    exceptionStmt.Try.Add(Expr.Snippet(string.Format("\t{0}", line)));
                } 
                // End If
                exceptionStmt.Try.Add(Expr.Snippet(string.Format("End If")));        
            }
        }

        private void AddMethods(IClassDeclaration classDeclaration)
        {
            if (classDeclaration == null) throw new ArgumentNullException("classDeclaration");

            foreach (MethodDefinition method in model.Methods)
            {
                // Get the method declaration
                MethodDeclaration methodDec = classDeclaration.AddMethod(method.Name);

                // Make public 
                methodDec.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                // Set return value of method
                methodDec.Signature.ReturnType = new TypeTypeDeclaration(method.Signature.ReturnValue);

                // Add parameter
                foreach (MethodParameter param in method.Signature.Parameters)
                {
                    ParameterDeclaration paramDec = methodDec.Signature.Parameters.Add(param.Type, param.Name);
                    paramDec.Direction = param.Direction;
                }

                // Set the method body
                methodDec.Body.Add(new SnippetExpression(method.Body));
            }
        }

        private void AddTables(IClassDeclaration classDeclaration)
        {
            foreach (TableDefinition table in TableManager.Instance.Tables)
            {
                // Add a property
                PropertyDeclaration prop = classDeclaration.AddProperty(typeof(TableDefinition), table.TableName);
                // Only a getter
                prop.Direction = FieldDirection.Out;
                // It must be public
                prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                
                // Now add the return value
                Expression tableMgrInstanceTables = Expr.Type(typeof(TableManager)).Method("Instance").Prop("Tables").Item(Expr.Prim(table.TableName));
                prop.Get.Return(tableMgrInstanceTables);
            }
        }

        private static bool IsPrimitiveDatatype(Type type)
        {
            return
                ((typeof (bool) == type) || (typeof (string) == type) || (typeof (char) == type) ||
                 (typeof (byte) == type) || (typeof (short) == type) ||
                 (typeof (int) == type) || (typeof (long) == type) || (typeof (float) == type) ||
                 (typeof (double) == type) || (typeof (decimal) == type));
        }
    }
}
